package inspect

import (
	"context"
	"fmt"
	"os"
	"strings"
	"time"

	"buf.build/gen/go/safedep/api/grpc/go/safedep/services/malysis/v1/malysisv1grpc"
	malysisv1pb "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/malysis/v1"
	packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
	malysisv1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/services/malysis/v1"
	"github.com/jedib0t/go-pretty/v6/table"
	"github.com/safedep/dry/adapters"
	"github.com/safedep/dry/api/pb"
	"github.com/safedep/dry/utils"
	"github.com/safedep/vet/internal/analytics"
	"github.com/safedep/vet/internal/auth"
	"github.com/safedep/vet/internal/ui"
	"github.com/safedep/vet/pkg/common/registry"
	vetutils "github.com/safedep/vet/pkg/common/utils"
	"github.com/safedep/vet/pkg/malysis"
	"github.com/safedep/vet/pkg/reporter"
	"github.com/spf13/cobra"
)

var (
	malwareAnalysisPackageUrl string
	malwareAnalysisTimeout    time.Duration
	malwareAnalysisReportJSON string
	malwareAnalysisReportOSV  string
	malwareAnalysisNoWait     bool

	malwareReportOSVFinderName   string
	malwareReportOSVContacts     []string
	malwareReportOSVReferenceURL string
	malwareReportOSVUseRange     bool
)

func newPackageMalwareInspectCommand() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "malware",
		Short: "Inspect an OSS package for malware",
		Long:  `Inspect an OSS package for malware using SafeDep Malware Analysis API`,
		RunE: func(cmd *cobra.Command, args []string) error {
			err := executeMalwareAnalysis()
			if err != nil {
				ui.PrintError("Failed: %v", err)
			}

			return nil
		},
	}

	cmd.Flags().StringVar(&malwareAnalysisPackageUrl, "purl", "",
		"Package URL to inspect for malware")
	cmd.Flags().DurationVar(&malwareAnalysisTimeout, "timeout", 5*time.Minute,
		"Timeout for malware analysis")
	cmd.Flags().StringVar(&malwareAnalysisReportJSON, "report-json", "",
		"Path to save malware analysis report in JSON format")
	cmd.Flags().StringVar(&malwareAnalysisReportOSV, "report-osv", "",
		"Dir path to save malware analysis report in OSV format and ossf/malicious-packages format")
	cmd.Flags().BoolVar(&malwareAnalysisNoWait, "no-wait", false,
		"Do not wait for malware analysis to complete")
	cmd.Flags().StringVar(&malwareReportOSVFinderName, "report-osv-finder-name", "",
		"Finder name for malware analysis report in OSV format")
	cmd.Flags().StringSliceVar(&malwareReportOSVContacts, "report-osv-contacts", []string{},
		"Contacts for malware analysis report in OSV format (URL, email, etc.)")
	cmd.Flags().StringVar(&malwareReportOSVReferenceURL, "report-osv-reference-url", "",
		"Custom reference URL for malware analysis report (defaults to app.safedep.io)")
	cmd.Flags().BoolVar(&malwareReportOSVUseRange, "report-osv-with-ranges", false,
		"Use range-based versioning in OSV report (default: use explicit versions)")

	_ = cmd.MarkFlagRequired("purl")

	return cmd
}

func executeMalwareAnalysis() error {
	analytics.TrackCommandInspectMalwareAnalysis()

	err := auth.Verify()
	if err != nil {
		return fmt.Errorf("access to Malicious Package Analysis requires an API key. " +
			"For more details: https://docs.safedep.io/cloud/quickstart/")
	}

	cc, err := auth.MalwareAnalysisClientConnection("malware-analysis")
	if err != nil {
		return err
	}

	service := malysisv1grpc.NewMalwareAnalysisServiceClient(cc)

	purl, err := pb.NewPurlPackageVersion(malwareAnalysisPackageUrl)
	if err != nil {
		return err
	}

	githubClient, err := adapters.NewGithubClient(adapters.DefaultGitHubClientConfig())
	if err != nil {
		return fmt.Errorf("failed to create GitHub client: %v", err)
	}

	versionResolver, err := registry.NewPackageVersionResolver(githubClient)
	if err != nil {
		return fmt.Errorf("failed to create package version resolver: %v", err)
	}

	packageVersion := purl.PackageVersion()

	// If package version is empty or latest replace it with actual literal latest version
	// Reference: https://github.com/safedep/vet/issues/446
	if packageVersion.GetVersion() == "" || packageVersion.GetVersion() == "latest" {
		ui.PrintMsg("Resolving package version")
		version, err := versionResolver.ResolvePackageLatestVersion(purl.Ecosystem(), purl.Name())
		if err != nil {
			return fmt.Errorf("failed to resolve package latest version: %v", err)
		}

		ui.PrintSuccess("Resolved package version: %s", version)
		packageVersion.Version = version
	}

	ctx := context.Background()
	ctx, cancelFun := context.WithTimeout(ctx, malwareAnalysisTimeout)

	defer cancelFun()

	// For GitHub Actions packages, we need to resolve the commit hash
	if packageVersion.GetPackage().GetEcosystem() == packagev1.Ecosystem_ECOSYSTEM_GITHUB_ACTIONS {
		ui.PrintMsg("Resolving commit hash for GitHub Actions package")

		commitHash, err := resolveGitHubActionsCommitHash(ctx, packageVersion)
		if err != nil {
			return fmt.Errorf("failed to resolve commit hash for GitHub Actions package: %v", err)
		}

		ui.PrintSuccess("Resolved commit hash for GitHub Actions package: %s", commitHash)
		packageVersion.Version = commitHash
	}

	analyzePackageResponse, err := service.AnalyzePackage(ctx, &malysisv1.AnalyzePackageRequest{
		Target: &malysisv1pb.PackageAnalysisTarget{
			PackageVersion: packageVersion,
		},
	})
	if err != nil {
		return fmt.Errorf("failed to submit package for malware analysis: %v", err)
	}

	ui.PrintMsg("Submitted package for malware analysis with ID: %s",
		analyzePackageResponse.GetAnalysisId())

	if malwareAnalysisNoWait {
		return nil
	}

	ui.StartSpinner("Waiting for malware analysis to complete")
	var report *malysisv1pb.Report
	var verificationRecord *malysisv1pb.VerificationRecord

	for {
		reportResponse, err := service.GetAnalysisReport(ctx, &malysisv1.GetAnalysisReportRequest{
			AnalysisId: analyzePackageResponse.GetAnalysisId(),
		})
		if err != nil {
			return fmt.Errorf("failed to get malware analysis report: %v", err)
		}

		if reportResponse.GetStatus() == malysisv1.AnalysisStatus_ANALYSIS_STATUS_FAILED {
			return fmt.Errorf("malware analysis failed: %s", reportResponse.GetErrorMessage())
		}

		if reportResponse.GetStatus() == malysisv1.AnalysisStatus_ANALYSIS_STATUS_COMPLETED {
			report = reportResponse.GetReport()
			verificationRecord = reportResponse.GetVerificationRecord()
			break
		}

		time.Sleep(5 * time.Second)
	}

	ui.StopSpinner()

	if report == nil {
		return fmt.Errorf("malware analysis report is empty")
	}

	ui.PrintSuccess("Malware analysis completed successfully")

	if malwareAnalysisReportJSON != "" {
		ui.PrintMsg("Generating JSON report")

		err = writeJSONReport(report)
		if err != nil {
			ui.PrintError("Failed to render malware analysis report in JSON format: %v", err)
		}
	}

	if malwareAnalysisReportOSV != "" {
		if !report.GetInference().GetIsMalware() {
			ui.PrintWarning("Report is not malware, skipping OSV report generation")
			return nil
		} else {
			ui.PrintMsg("Generating OSV report in: %s", malwareAnalysisReportOSV)

			err = writeOSVReport(report)
			if err != nil {
				ui.PrintError("Failed to render malware analysis report in OSV format: %v", err)
			}
		}
	}

	return renderMalwareAnalysisReport(malwareAnalysisPackageUrl,
		analyzePackageResponse.GetAnalysisId(), report, verificationRecord)
}

func writeOSVReport(report *malysisv1pb.Report) error {
	err := os.MkdirAll(malwareAnalysisReportOSV, 0o755)
	if err != nil {
		return fmt.Errorf("failed to create directory: %v", err)
	}

	generator, err := malysis.NewOpenSSFMaliciousPackageReportGenerator(malysis.OpenSSFMaliciousPackageReportGeneratorConfig{
		Dir: malwareAnalysisReportOSV,
	})
	if err != nil {
		return fmt.Errorf("failed to create OpenSSF malicious package report generator: %v", err)
	}

	err = generator.GenerateReport(context.Background(), report, malysis.OpenSSFMaliciousPackageReportParams{
		FinderName:   malwareReportOSVFinderName,
		Contacts:     malwareReportOSVContacts,
		ReferenceURL: malwareReportOSVReferenceURL,
		UseRange:     malwareReportOSVUseRange,
	})
	if err != nil {
		return fmt.Errorf("failed to generate OpenSSF malicious package report: %v", err)
	}

	return nil
}

func writeJSONReport(report *malysisv1pb.Report) error {
	data, err := utils.ToPbJson(report, "  ")
	if err != nil {
		return err
	}

	return os.WriteFile(malwareAnalysisReportJSON, []byte(data), 0o644)
}

func renderMalwareAnalysisReport(purl string, analysisId string,
	report *malysisv1pb.Report, vr *malysisv1pb.VerificationRecord,
) error {
	ui.PrintMsg("Malware analysis report for package: %s", purl)

	tbl := table.NewWriter()
	tbl.SetOutputMirror(os.Stdout)
	tbl.SetStyle(table.StyleLight)

	tbl.AppendHeader(table.Row{"Package URL", "Status", "Confidence"})

	status := reporter.InfoBgText(" SAFE ")
	if report.GetInference().GetIsMalware() {
		if vr != nil && vr.IsMalware {
			status = reporter.CriticalBgText(" MALICIOUS ")
		} else {
			status = reporter.WarningBgText(" SUSPICIOUS ")
		}
	}

	confidence := report.GetInference().GetConfidence().String()
	confidence = strings.TrimPrefix(confidence, "CONFIDENCE_")

	tbl.AppendRow(table.Row{purl, status, confidence})
	tbl.Render()

	fmt.Println()
	fmt.Println(reporter.WarningText(fmt.Sprintf("** The full report is available at: %s",
		reportVisualizationUrl(analysisId))))
	fmt.Println()

	return nil
}

func reportVisualizationUrl(analysisId string) string {
	return malysis.ReportURL(analysisId)
}

func resolveGitHubActionsCommitHash(ctx context.Context, packageVersion *packagev1.PackageVersion) (string, error) {
	gha, err := adapters.NewGithubClient(adapters.DefaultGitHubClientConfig())
	if err != nil {
		return "", fmt.Errorf("failed to create GitHub client: %v", err)
	}

	parts := strings.Split(packageVersion.GetPackage().GetName(), "/")
	if len(parts) != 2 {
		return "", fmt.Errorf("invalid repository name: %s - should be in the format <owner>/<repo>", packageVersion.GetPackage().GetName())
	}

	owner := parts[0]
	repo := parts[1]

	return vetutils.ResolveGitHubRepositoryCommitSHA(ctx,
		gha, owner, repo, packageVersion.GetVersion())
}
